Skip to content

fix(filesystem): write in place to preserve file identity on edits#4515

Open
ayushbunny11 wants to merge 1 commit into
modelcontextprotocol:mainfrom
ayushbunny11:fix/filesystem-preserve-file-identity
Open

fix(filesystem): write in place to preserve file identity on edits#4515
ayushbunny11 wants to merge 1 commit into
modelcontextprotocol:mainfrom
ayushbunny11:fix/filesystem-preserve-file-identity

Conversation

@ayushbunny11

Copy link
Copy Markdown

write_file/edit_file used a temp-file+rename strategy for existing files, which replaces the inode on every write and destroys the file's creation time, hard links, and inode-based file watches.

On POSIX, write in place instead: open with O_RDWR | O_NOFOLLOW to keep the same symlink protection the rename gave us, fstat to confirm the target is still a regular file, then truncate and write. This trades away crash-atomicity (a crash mid-write can now leave a truncated file, where rename left the original untouched).

On win32, keep the rename strategy: O_NOFOLLOW isn't reliably enforced there, and symlink creation already requires elevated privileges by default, so the threat model differs.

Fixes #4512

Description

write_file/edit_file used a temp-file+rename strategy for existing files, which replaces the inode on every write and destroys the file's creation time, hard links, and inode-based file watches.

On POSIX, write in place instead: open with O_RDWR | O_NOFOLLOW to keep the same symlink protection the rename gave us, fstat to confirm the target is still a regular file, then truncate and write. This trades away crash-atomicity (a crash mid-write can now leave a truncated file, where rename left the original untouched).

On win32, keep the rename strategy: O_NOFOLLOW isn't reliably enforced there, and symlink creation already requires elevated privileges by default, so the threat model differs.

Fixes #4512

Server Details

  • Server: filesystem
  • Changes to: tools (write_file, edit_file)

Motivation and Context

The server's own get_file_info tool reports a file's birthtime, but every edit through write_file/edit_file silently reset it, because the rename-based write replaced the file's inode. This also broke hard links (a link to the file kept pointing at stale pre-edit content) and confused inode-based file watchers, which saw "delete + create" instead of "modify" on every edit.

How Has This Been Tested?

  • npm run build passes (typecheck).
  • npm test passes — 157/157, including new coverage for the EEXIST in-place path, the non-regular-file rejection, and the win32 rename fallback.
  • Manually verified on a real filesystem (Linux) that inode and birthtime are preserved across an in-place write, and that opening a symlink with O_NOFOLLOW correctly fails with ELOOP.
  • Tested with Claude Code as an MCP client against the built server: registered the local build, ran edit_file and write_file against a real file with a hard link, and confirmed inode/birthtime stayed constant across edits while the hard link tracked the change.

Breaking Changes

None. Tool inputs/outputs are unchanged; this only changes the write mechanism on disk. The only observable behavior difference is that crashes mid-write can now leave a truncated file on POSIX, instead of leaving the original untouched.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options

write_file/edit_file used a temp-file+rename strategy for existing
files, which replaces the inode on every write and destroys the
file's creation time, hard links, and inode-based file watches.

On POSIX, write in place instead: open with O_RDWR | O_NOFOLLOW to
keep the same symlink protection the rename gave us, fstat to
confirm the target is still a regular file, then truncate and write.
This trades away crash-atomicity (a crash mid-write can now leave a
truncated file, where rename left the original untouched).

On win32, keep the rename strategy: O_NOFOLLOW isn't reliably
enforced there, and symlink creation already requires elevated
privileges by default, so the threat model differs.

Fixes modelcontextprotocol#4512
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filesystem: write_file/edit_file destroy file creation time (birthtime) and file identity due to atomic-rename write strategy

1 participant